注意! 注意! 注意!
此篇程式碼為分享範例,請勿直接執行程式碼。若想測試爬蟲,請以其他目標網站測試,並遵守爬蟲禮儀,謝謝。
歡迎來到第六天,今天要承接第四天衍伸到更深層的爬蟲。一個網站(這裡定義為同一個域名底下的所有網頁)不會只有單純的首頁與文章。像 IT 邦幫忙是一個很強大的網站,包含了很多有趣、很強大的功能,但這裡只簡單的舉其中一個類型 - 技術問答,在 IT 邦幫忙中累積了無數前輩們的討論與文章,若今天的爬蟲目的為爬取所有問題內容,那我們該怎麼處理呢?
經過了前兩篇的分享,在爬蟲時的第一個動作是定義好目標,接下來就是觀察目標頁面的架構。首先我們目標為爬取所有問答相關的內容,會發現在「技術問答」這個大標底下,又有「最新」、「熱門」、「已解決」這三個分類,而在分類底下又有許多頁。而隨意點進一篇問答文章時會發現問答類型文章的 URL 架構為 https://ithelp.ithome.com.tw/questions/{文章 ID}
。再了解目標頁面的架構與相互關係後,就可以開始擬定爬蟲策略。
最直觀的方式是模擬人類行為做爬蟲,也就是順著架構往下一層層的爬。另一種是盲目式的爬,由於我們觀察到每一個頁面中,都有另外推薦文章、熱門文章等,因此我們也可以設定一隻蟲是收集整個頁面的 <a>
,一但滿足https://ithelp.ithome.com.tw/questions/{文章 ID}
的條件,就進行爬蟲,一路往下爬。今天我會以第一種策略作為舉例。
首先「最新」、「熱門」、「已解決」這三個分類是固定的,因此可以設定這三個分頁為目標網址,但每個分類底下的頁面數並非固定,這會是我們需要另外著墨的功能,另外由於文章數過多,無法保證這三個分類內的文章不會相互重複,因此要另外設定一個檢查機制,去避免浪費請求資源。
依照我們的策略,首先第一步是蒐集所有問答文章的 URL,而在這個步驟由於每個分類所擁有的頁面不同,因此需要解決如何完整讀取所有頁面的資料。由於 IT 邦幫忙的頁面中會顯示該分類的最後一頁頁碼,因此可以直接讀取數字並透過 GET
的 Parameters 進行完整的爬蟲。當然另一種方式就是以「下一頁」做為目標,不斷的讀取該標籤的 URL 往下爬。這裡將以第一種方式做舉例。
import requests
from lxml import etree
def page_numbers(html):
next_tag = html.xpath("//a[@rel='next']")[0] # 定位出下一頁
final_page = next_tag.getparent().getprevious().find('a').text # 下一頁按鈕的前一個按鈕及為最後一頁的數字
return int(final_page)
當我們解決尋找最後一頁,接下來要處理的是擷取每個分類的文章網址。
def get_article_url(url, final_page):
urls = set()
for i in range(2, final_page+1):
response = requests.get(url, params = {'page':i})
html = etree.HTML(response.text)
urls = urls.union(set(html.xpath("//a[@class = 'qa-list__title-link']/@href")))
return urls
接下來就可以將所有程式碼合併。
from time import sleep
import requests
from lxml import etree
def page_numbers(html):
next_tag = html.xpath("//a[@rel='next']")[0] # 定位出下一頁
final_page = next_tag.getparent().getprevious().find('a').text # 下一頁按鈕的前一個按鈕及為最後一頁的數字
return int(final_page)
def get_article_url(url, last_page):
urls = set()
for i in range(last_page):
response = requests.get(url, params = {'page':i+1})
html = etree.HTML(response.text)
urls = urls.union(set(html.xpath("//a[@class = 'qa-list__title-link']/@href")))
sleep(100) # 做隻有禮貌的蟲
return urls
target_url = ["https://ithelp.ithome.com.tw/questions",
"https://ithelp.ithome.com.tw/questions?tab=hot",
"https://ithelp.ithome.com.tw/questions?tab=solved"]
article_urls = set()
for url in target_url:
response = requests.get(url)
html = etree.HTML(response.text)
last_page = page_numbers(html)
article_urls = article_urls.union(get_article_url(url,last_page))
之後我們就可以到目前為止我們就拿到所有問答文章的 URL,由於這次在抓取文章 URL 時沒有同時間把標題留下,因此接下來需要整理之前寫的程式碼
def article_parse(link):
response = requests.get(link)
html = etree.HTML(response.text)
mk = html.xpath("//div[@class='markdown__style']")[0]
header = html.xpath("//h2[@class = 'qa-header__title']/text()")[0]
return {header : etree.tostring(mk, method='text', encoding='unicode', pretty_print=True).strip()}
最後合併所有的程式碼
from time import sleep
import requests
from lxml import etree
def page_numbers(html):
next_tag = html.xpath("//a[@rel='next']")[0] # 定位出下一頁
final_page = next_tag.getparent().getprevious().find('a').text # 下一頁按鈕的前一個按鈕及為最後一頁的數字
return int(final_page)
def get_article_url(url, last_page):
urls = set()
for i in range(last_page):
response = requests.get(url, params = {'page':i+1})
html = etree.HTML(response.text)
urls = urls.union(set(html.xpath("//a[@class = 'qa-list__title-link']/@href")))
sleep(100) # 做隻有禮貌的蟲
return urls
def article_parse(link):
response = requests.get(link)
html = etree.HTML(response.text)
mk = html.xpath("//div[@class='markdown__style']")[0]
header = html.xpath("//h2[@class = 'qa-header__title']/text()")[0]
return {header : etree.tostring(mk, method='text', encoding='unicode', pretty_print=True).strip()}
target_url = ["https://ithelp.ithome.com.tw/questions",
"https://ithelp.ithome.com.tw/questions?tab=hot",
"https://ithelp.ithome.com.tw/questions?tab=solved"]
article_urls = set()
for url in target_url:
response = requests.get(url)
html = etree.HTML(response.text)
last_page = page_numbers(html)
article_urls = article_urls.union(get_article_url(url,last_page))
article = {}
for url in article_urls:
article = {**article,**article_parse(url)}
sleep(100) # 做隻有禮貌的蟲
這樣就完成所有的程式碼了!今天就到這裡,明天見!